﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
using Xceed.Silverlight.Compatibility;
using Xceed.Silverlight.DragDrop;
using System.Windows.Controls.Primitives;

namespace Xceed.Silverlight.DataGrid
{
  public partial class DataGridControl : IDropTargetComplement
  {
    private const double AutoScrollSectionWidth = 30;
    private const int AutoScrollMovementDelay = 50; // in ms
    private const double AutoScrollMovementUnits = 0.1;

    protected virtual DragDropContext CreateDragContextCore( ColumnManagerCell cell )
    {
      DataTemplate dragDropConfigurationTemplate = this.GetDragDropConfigurationTemplate();

      DraggedElementConfiguration draggedElementConfiguration = new DraggedElementConfiguration();

      draggedElementConfiguration.ElementTemplate = dragDropConfigurationTemplate;
      draggedElementConfiguration.SiblingElementTemplate = dragDropConfigurationTemplate;

      IEnumerable<FrameworkElement> siblingDraggedElements = this.GetSiblingDraggedElements( cell );

      DragDropContext dragDropContext = new DragDropContext( cell, draggedElementConfiguration, siblingDraggedElements );

      dragDropContext.DragStarting += new EventHandler<DragDropCancelEventArgs>( this.OnDragStarting );
      dragDropContext.DragStarted += new EventHandler( this.OnDragStarted );
      dragDropContext.DragMove += new EventHandler<DragDropMoveEventArgs>( this.OnDragMoved );
      dragDropContext.DragCompleted += new EventHandler<DragDropEventArgs>( this.OnDragEnded );
      dragDropContext.DragAborted += new EventHandler<DragDropEventArgs>( this.OnDragAborted );
      dragDropContext.DragPaused += new EventHandler<DragDropEventArgs>( this.OnDragPaused );
      dragDropContext.DragResumed += new EventHandler<DragDropEventArgs>( this.OnDragResumed );

      return dragDropContext;
    }

    private IEnumerable<FrameworkElement> GetSiblingDraggedElementsCore( ColumnManagerCell columnManagerCell )
    {
      IList<FrameworkElement> draggedCells = new List<FrameworkElement>();
      IList<IColumnElementHost> renderedRows = new List<IColumnElementHost>();

      RowVisitor visitor = new RowVisitor( ChildrenPolicy.RenderedChildrenOnly, ( row ) => renderedRows.Add( row ) );
      this.DataGridPanel.Accept( visitor );

      foreach( IColumnElementHost row in renderedRows )
      {
        ColumnElementWrapper colElementWrapper = this.GetColumnElement( row, columnManagerCell.ParentColumnContainer );
        if( colElementWrapper != null )
        {
          Cell correspondingCell = ( Cell )colElementWrapper.Child;
          if( !object.Equals( correspondingCell, columnManagerCell ) && correspondingCell.FollowsParentColumnDrag )
          {
            draggedCells.Add( ( FrameworkElement )correspondingCell );
          }
        }
      }

      return draggedCells;
    }

    private void OnColumnResizingThumbDrag( object sender, XceedRoutedEventArgs args )
    {
      XceedRoutedEventThumbDragDeltaArgs deltaDragArgs = ( XceedRoutedEventThumbDragDeltaArgs )args;
      ColumnManagerCell cell = ( ( ColumnManagerCell )deltaDragArgs.OriginalSource );

      Column parentColumn = cell.ParentColumn;
      double horizontalChange = deltaDragArgs.DragDeltaData.HorizontalChange;

      if( parentColumn == null )
        return;

      // The current colum is at its minimum width; therefore, make sure the mouse cursor can't resize the column 
      if( parentColumn.ActualWidth == ColumnManagerCell.MinimumColumnWidth )
      {
        if( cell.IsColumnNegativeResizingThumb( horizontalChange ) )
          return;
      }

      double newWidth = Math.Max( ColumnManagerCell.MinimumColumnWidth, parentColumn.ActualWidth + horizontalChange );
      newWidth = Math.Max( this.GetInheritedColumnMinWidth( parentColumn ), newWidth );
      newWidth = Math.Min( this.GetInheritedColumnMaxWidth( parentColumn ), newWidth );

      parentColumn.Width = newWidth;
    }

    private void PrepareDrag( ColumnManagerCell columnManagerCell, MouseButtonEventArgs e )
    {
      DragDropManager.PrepareDrag( this.CreateDragContext( columnManagerCell ), e );
    }

    private GroupDescription GetGroupDescriptionForColumn( Column column )
    {
      // If the column already has a GroupDescription, return it!
      if( column.GroupDescription != null )
        return column.GroupDescription;

      var count = this.DataGridContext.GroupDescriptions.Count;
      for( var i = 0; i < count; i++ )
      {
        // We want to see if the column is already grouped. In that case, return the corresponding GroupDescription.
        var groupDescription = this.DataGridContext.GroupDescriptions[ i ] as PropertyGroupDescription;

        if( ( groupDescription != null )
          && ( String.Compare( groupDescription.PropertyName, column.DisplayMemberBindingInfo.Path.Path, StringComparison.InvariantCultureIgnoreCase ) == 0 ) )
        {
          return groupDescription;
        }
      }

      // No GroupDescription has been found. Return a new one!
      return new DataGridGroupDescription( column.DisplayMemberBindingInfo.Path.Path );
    }

    private DragDropContext CreateDragContext( ColumnManagerCell columnManagerCell )
    {
      return this.CreateDragContextCore( columnManagerCell );
    }

    private DataTemplate GetDragDropConfigurationTemplate()
    {
      string dragConfigurationTemplateString = "<DataTemplate xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns:sldg='http://schemas.xceed.com/silverlight/xaml/datagrid'><Image Source='{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(sldg:Cell.DraggedContentImageSource)}'/></DataTemplate>";

      return CompatibilityUtils.XamlParse( dragConfigurationTemplateString ) as DataTemplate;
    }

    private IEnumerable<FrameworkElement> GetSiblingDraggedElements( ColumnManagerCell cell )
    {
      return this.GetSiblingDraggedElementsCore( cell );
    }

    private void MimicParentRowDragOver( DragDropContext dragContext, FrameworkElement draggedElement )
    {
      Cell draggedCell = draggedElement as Cell;
      if( draggedCell == null )
        return;

      //Mimic a DragOver on the parent row so it may moves columns 
      //while the dragging column isn't over it.
      Row parentRow = draggedCell.ParentRow;
      IDropTarget dropTarget = parentRow as IDropTarget;

      //The parent row must be a valid drop target.
      if( dropTarget == null || !parentRow.IsHitTestVisible )
        return;

      dropTarget.DragOver( dragContext, dragContext.LastMouseMoveEventArgs.GetPosition( parentRow ) );
    }








    private void OnDragStarting( object sender, DragDropCancelEventArgs e )
    {
    }

    private void OnDragStarted( object sender, EventArgs e )
    {
      DragDropContext dragContext = ( DragDropContext )sender;
      ColumnManagerCell columnManagerCell = ( ColumnManagerCell )dragContext.DraggedElement;

      columnManagerCell.IsPressed = false;
      columnManagerCell.ParentColumnContainer.IsDragged = true;
      columnManagerCell.ParentColumnContainer.IsDragPaused = false;
    }

    private void OnDragMoved( object sender, DragDropMoveEventArgs e )
    {
      ColumnManagerCell cell = ( ColumnManagerCell )e.DragDropContext.DraggedElement;
      ColumnManagerRow parentRow = ( ColumnManagerRow )cell.ParentRow;

      // We only want to mimic a drag over the parent row
      // if we are not over a IDropTarget.
      IDropTarget currentDropTarget = e.DragDropContext.CurrentDropTarget;
      if( currentDropTarget != null )
        return;

      if( ( !cell.ParentColumnContainer.IsDragPaused )
        && ( parentRow.AllowColumnReorder.GetValueOrDefault( this.AllowColumnReorder ) ) )
      {
        // User moved a columnManagerCell over something other than a ColumnManagerRow; mimic a column drag.
        this.MimicParentRowDragOver( e.DragDropContext, cell );
      }

      //Force the cursor so an arrow is displayed even on a no drop zone
      e.Cursor = DragDropCursor.Arrow;
    }

    private void OnDragEnded( object sender, DragDropEventArgs e )
    {
      this.EndDrag( e.DragDropContext.DraggedElement );
    }

    private void EndDrag( FrameworkElement draggedElement )
    {
      ColumnManagerCell columnManagerCell = ( ColumnManagerCell )draggedElement;
      columnManagerCell.ParentColumnContainer.IsDragged = false;
      columnManagerCell.ParentColumnContainer.IsDragPaused = false;
      this.UpdateColumnElementCommonState( columnManagerCell, false, columnManagerCell.IsMouseOver, false, true );

      this.StopAutoScroll();
    }

    private void OnDragAborted( object sender, DragDropEventArgs e )
    {
      this.EndDrag( e.DragDropContext.DraggedElement );
    }

    private void OnDragPaused( object sender, DragDropEventArgs e )
    {
      ColumnManagerCell columnManagerCell = ( ColumnManagerCell )e.DragDropContext.DraggedElement;
      columnManagerCell.ParentColumnContainer.IsDragPaused = true;
    }

    private void OnDragResumed( object sender, DragDropEventArgs e )
    {
      ColumnManagerCell columnManagerCell = ( ColumnManagerCell )e.DragDropContext.DraggedElement;
      columnManagerCell.ParentColumnContainer.IsDragPaused = false;
    }

    private void UpdateColumnVisiblePositionOnDragOverColumnManagerRow( ColumnManagerCell cell, Point mousePosition )
    {
      if( cell == null )
        return;

      Column parentColumn = cell.ParentColumn;
      ColumnManagerRow parentRow = ( ColumnManagerRow )cell.ParentRow;

      // Remove the the CellsPanelOffset and the CellsPanelPosition from the current mouse 
      // horizontal location to obtain a position relative to column themselves.
      GeneralTransform cellsPanelTransform = parentRow.CellsHost.TransformToVisual( this );
      Point cellsPanelOriginOffset = cellsPanelTransform.Transform( new Point( 0d, 0d ) );

      double actualOffset = mousePosition.X - cellsPanelOriginOffset.X;

      int visibleIndexForPosition = -1;

      foreach( ColumnContainer columnContainer in this.DataGridPanel.RenderedColumns )
      {
        Column column = columnContainer.Column;

        // Get the position of the columnContainer's columnManagerCell relative to top-left of DataGrid
        GeneralTransform columnOffsetTransform = ( ( UIElement )columnContainer.ColumnManagerCells.First() ).TransformToVisual( this );
        Point columnOffsetPoint = columnOffsetTransform.Transform( new Point( 0d, 0d ) );
        double columnOffset = columnOffsetPoint.X - cellsPanelOriginOffset.X;

        if( ( object.Equals( column, parentColumn ) )
         && ( actualOffset >= columnOffset )
         && ( actualOffset <= columnOffset + column.ActualWidth ) )
        {
          visibleIndexForPosition = column.VisiblePosition;
          break;
        }
        else if( ( column.VisiblePosition < parentColumn.VisiblePosition )
             && ( actualOffset >= columnOffset )
             && ( actualOffset <= ( columnOffset + ( column.ActualWidth / 2 ) ) ) )
        {
          visibleIndexForPosition = column.VisiblePosition;
          break;
        }
        else if( ( column.VisiblePosition > parentColumn.VisiblePosition )
              && ( actualOffset >= columnOffset )
              && ( actualOffset >= ( columnOffset + ( column.ActualWidth / 2 ) ) ) )
        {
          visibleIndexForPosition = column.VisiblePosition;
          break;
        }
      }

      if( visibleIndexForPosition > -1 )
      {
        int newVisiblePosition = Math.Min( visibleIndexForPosition, this.DataGridContext.Columns.Count - 1 );
        if( newVisiblePosition != parentColumn.VisiblePosition )
        {
          int oldVisiblePosition = parentColumn.VisiblePosition;

          // Update the column to it's new position.
          parentColumn.VisiblePosition = newVisiblePosition;

          this.UpdateFixedColumnsCount( parentColumn, oldVisiblePosition, newVisiblePosition );
        }
      }
    }

    private void UpdateFixedColumnsCount( Column column, int oldVisiblePosition, int newVisiblePosition )
    {
      // Invisible columns have no effect on the fixed columns.
      if( !column.Visible )
        return;

      int leftFixedColumnVisiblePosition = this.LeftFixedColumnCount;
      int rightFixedColumnVisiblePosition = ( from c in this.Columns
                                              where c.Visible
                                              select c ).Count() - this.RightFixedColumnCount;
      bool leaveLeftSection = ( oldVisiblePosition < leftFixedColumnVisiblePosition )
                           && ( newVisiblePosition >= leftFixedColumnVisiblePosition );
      bool enterLeftSection = ( oldVisiblePosition >= leftFixedColumnVisiblePosition )
                           && ( newVisiblePosition < leftFixedColumnVisiblePosition );
      bool leaveRightSection = ( oldVisiblePosition >= rightFixedColumnVisiblePosition )
                            && ( newVisiblePosition < rightFixedColumnVisiblePosition );
      bool enterRightSection = ( oldVisiblePosition < rightFixedColumnVisiblePosition )
                            && ( newVisiblePosition >= rightFixedColumnVisiblePosition );

      if( leaveLeftSection || enterLeftSection )
      {
        this.IncrementLeftFixedColumnCount( enterLeftSection );
      }

      if( leaveRightSection || enterRightSection )
      {
        this.IncrementRightFixedColumnCount( enterRightSection );
      }
    }









    private bool CanDropElementOnTarget( ColumnManagerRow columnManagerRow, DragDropContext dragContext )
    {
      ColumnManagerCell columnManagerCell = dragContext.DraggedElement as ColumnManagerCell;

      if( ( columnManagerCell == null ) || ( columnManagerRow == null ) )
        return false;

      return columnManagerRow.AllowColumnReorder.GetValueOrDefault( this.AllowColumnReorder );
    }

















    private void DragOverTarget( ColumnManagerRow columnManagerRow, DragDropContext dragContext, Point mousePosition )
    {
      //The dragged element must be a ColumnManagerCell and dropable.
      ColumnManagerCell draggedCell = dragContext.DraggedElement as ColumnManagerCell;

      if( draggedCell == null || !( ( IDropTarget )columnManagerRow ).CanDropElement( dragContext ) )
        return;

      //Update the dragged element's position and auto scroll if it's needed.
      this.UpdateColumnVisiblePositionOnDragOverColumnManagerRow( draggedCell, mousePosition );
      this.DetectAutoScrollSection( mousePosition );
    }



















    private void DropOnTarget( ColumnManagerRow columnManagerRow, DragDropContext dragContext )
    {
      this.StopAutoScroll();
    }

    private void DetectAutoScrollSection( Point mousePosition )
    {
      double leftSide = 0d;
      double rightSide = this.DataGridPanel.ActualWidth;

      double fromLeftSide = mousePosition.X - leftSide;
      double fromRightSide = rightSide - mousePosition.X;

      if( fromLeftSide >= 0 && fromLeftSide <= DataGridControl.AutoScrollSectionWidth )
      {
        this.UpdateAutoScroll( ScrollDirections.Left );
      }
      else if( fromRightSide >= 0 && fromRightSide <= DataGridControl.AutoScrollSectionWidth )
      {
        this.UpdateAutoScroll( ScrollDirections.Right );
      }
      else
      {
        this.StopAutoScroll();
      }
    }

    private void UpdateAutoScroll( ScrollDirections direction )
    {
      //Stop the auto scrolling if no valid direction is given.
      if( direction == ScrollDirections.None )
      {
        this.StopAutoScroll();
        return;
      }

      if( !m_isAutoScrollStarted )
      {
        CompositionTarget.Rendering += this.OnRendering;
        m_autoScrollLastTimeStamp = Environment.TickCount;
        m_isAutoScrollStarted = true;
      }

      if( ( Environment.TickCount - m_autoScrollLastTimeStamp ) > DataGridControl.AutoScrollMovementDelay )
      {
        if( direction == ScrollDirections.Right )
        {
          double newScrollBarPosition = m_horizontalScrollBar.Value + DataGridControl.AutoScrollMovementUnits;
          if( newScrollBarPosition <= m_horizontalScrollBar.Maximum )
          {
            this.OnScroll( new ScrollEventArgs( ScrollEventType.ThumbTrack, newScrollBarPosition ),
                         ref m_lastHorizontalValue,
                         this.SetColumns,
                         true );
          }
        }
        else if( direction == ScrollDirections.Left )
        {
          double newScrollBarPosition = m_horizontalScrollBar.Value - DataGridControl.AutoScrollMovementUnits;
          if( newScrollBarPosition >= m_horizontalScrollBar.Minimum )
          {
            this.OnScroll( new ScrollEventArgs( ScrollEventType.ThumbTrack, newScrollBarPosition ),
                       ref m_lastHorizontalValue,
                       this.SetColumns,
                       true );
          }
        }
        m_autoScrollLastTimeStamp = Environment.TickCount;
      }
    }

    private void StopAutoScroll()
    {
      if( m_isAutoScrollStarted )
      {
        m_isAutoScrollStarted = false;
        CompositionTarget.Rendering -= this.OnRendering;
      }
    }

    private void OnRendering( object sender, EventArgs e )
    {
      if( !m_isAutoScrollStarted )
        return;

      //Emit a drag over on the parent row in case the dragged column may
      //switch places with another column.  The OnDragMoved method will not
      //do the job if the mouse isn't moving and auto scrolling is active.
      DragDropContext dragContext = DragDropManager.CurrentDragDropContext;
      if( dragContext != null )
      {
        this.MimicParentRowDragOver( dragContext, dragContext.DraggedElement );
      }
    }

    #region IDropTargetComplement Interface

    bool IDropTargetComplement.CanDropElement( IDropTarget dropTarget, DragDropContext dragContext )
    {
        if( dropTarget is ColumnManagerRow )
          return this.CanDropElementOnTarget( ( ColumnManagerRow )dropTarget, dragContext );
        else
          throw new DataGridException( "The element was not recognized." );
    }

    void IDropTargetComplement.DragEnter( IDropTarget dropTarget, DragDropContext dragContext )
    {
        throw new DataGridException( "The element was not recognized." ); 
    }

    void IDropTargetComplement.DragOver( IDropTarget dropTarget, DragDropContext dragContext, Point mousePosition )
    {
      if( dropTarget is ColumnManagerRow )
        this.DragOverTarget( ( ColumnManagerRow )dropTarget, dragContext, mousePosition );
      else
        throw new DataGridException( "The element was not recognized as a ColumnManagerRow." ); 
    }

    void IDropTargetComplement.DragLeave( IDropTarget dropTarget, DragDropContext dragContext )
    {
        throw new DataGridException( "The element was not recognized." ); 
    }

    void IDropTargetComplement.Drop( IDropTarget dropTarget, DragDropContext dragContext )
    {
        if( dropTarget is ColumnManagerRow )
          this.DropOnTarget( ( ColumnManagerRow )dropTarget, dragContext );
        else
          throw new DataGridException( "The element was not recognized." ); 
    }

    #endregion

    private int m_autoScrollLastTimeStamp = 0;
    private bool m_isAutoScrollStarted = false;
  }
}
